Skip to content

docs: design for supporting deposits from CEX (DEFI-2096)#10652

Draft
gregorydemay wants to merge 13 commits into
masterfrom
ic_DEFI-2096_design-support-deposit-from-cex
Draft

docs: design for supporting deposits from CEX (DEFI-2096)#10652
gregorydemay wants to merge 13 commits into
masterfrom
ic_DEFI-2096_design-support-deposit-from-cex

Conversation

@gregorydemay

@gregorydemay gregorydemay commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Design doc for DEFI-2096: allow onramping ckUSDC/ckUSDT (and generally any ckERC20, later ckETH) directly from a centralized exchange withdrawal.

Deposits currently require calling the helper smart contract, which a CEX cannot do: a CEX withdrawal is a plain transfer from a shared hot wallet, carrying no IC principal. The proposed design gives each IC account a unique tECDSA-derived deposit address and sweeps funds to the minter using EIP-7702 (Pectra), so deposit addresses never need ETH for gas and remain key-recoverable independently of any contract code.

The doc covers the requirements, claim-based deposit detection, the sweeper delegate, fee model, a phased delivery (ckERC20 first, then ckETH with its balance-based detection and fixed-21k-gas constraints), and the discussed alternatives (CREATE2 forwarders, ERC-4337, permit-based sponsoring).

🤖 Generated with Claude Code

Design proposal for onramping ckERC20 (and later ckETH) directly from a
centralized exchange withdrawal: per-account tECDSA deposit addresses
swept via EIP-7702, so that depositors never need ETH for gas.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a design document describing how to support ckERC20 (and later ckETH) deposits directly from centralized exchanges by assigning each IC account a deterministic tECDSA-derived deposit address and sweeping funds to the minter using EIP-7702.

Changes:

  • Introduces a new design doc covering motivation, requirements, and phased rollout (ckERC20 first, then ckETH).
  • Specifies claim-based deposit detection, sweeping/delegation flow, fee model, and a test plan.
  • Documents considered alternatives (CREATE2 forwarders, ERC-4337, permit-based approaches) and rationale.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +29 to +30
directly to the minter address today are simply unaccounted, with no recovery path
(see the comment on `EthBalance::eth_balance` in `src/state.rs`).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Fixed in cc57879: the doc now references the documentation of the eth_balance field of the EthBalance struct in src/state.rs.

gregorydemay and others added 4 commits July 3, 2026 14:05
Foundry-based script (local anvil, Prague hardfork) demonstrating the
core mechanism: an unfunded minter-derived deposit EOA receives a plain
USDT-style ERC-20 transfer and is swept to the minter in a single
type-0x04 transaction, with all gas paid by the minter.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replaces the bash/cast demo with a standalone cargo binary that prints
the full sweep transaction details and asserts gas usage, and adds a
batched sweep: one type-0x04 transaction targeting several deposit EOAs
via a batch entry point on the CkSweeper delegate (measured ~26k
marginal gas per additional EOA vs ~67k for a standalone sweep). The
design doc is updated accordingly (batching no longer needs Multicall3).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Anvil's default hardfork is the latest supported one, which includes
EIP-7702 in any release since Pectra; reproducibility comes from
pinning the foundry image version, not from naming the fork.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment on lines +58 to +59
const MINTER_PK: &str = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
const CEX_PK: &str = "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d";

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: PK can be misunderstood for PUBLICK_KEY, which is not the case here. remove the abbrevation -> MINTER_PRIVATE_KEY

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Renamed to MINTER_PRIVATE_KEY / CEX_PRIVATE_KEY in cc57879.

Comment on lines +163 to +164
let mut wallet = EthereumWallet::from(minter_signer);
wallet.register_signer(cex_signer);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what has the minter_signer anything to do with the cex_signer? Shouldn't they be 2 separate wallets?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Agreed — they are unrelated parties. Split into two separate wallets/providers (minter_wallet/minter_provider and cex_wallet/cex_provider) in cc57879.

nonce: 0,
};
let signature = deposit_signer.sign_hash_sync(&authorization.signature_hash())?;
Ok(authorization.into_signed(signature))

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: print the full hex of the send transaction

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Done in cc57879: each sweep now prints the full raw signed transaction hex (type-0x04 payload including the authorization list) before sending it.

Comment on lines +263 to +266
ensure!(
(60_000..=90_000).contains(&single_gas),
"unexpected single-sweep gas: {single_gas}"
);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be deterministic, let's hard-code the gas used.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Hard-coded in cc57879 as SINGLE_SWEEP_GAS_USED = 66_889, asserted with equality.

Comment on lines +304 to +308
let batch_gas = batch_receipt.gas_used;
ensure!(
batch_gas < 3 * single_gas,
"batching brought no amortization: {batch_gas} vs 3 x {single_gas}"
);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also here hard-code the gas used.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Hard-coded in cc57879 as BATCH_SWEEP_GAS_USED = 118_982, asserted with equality.

ok("deposit addresses still have 0 ETH (cannot pay gas themselves)");

step("4) Minter sweeps ONE deposit address in ONE EIP-7702 transaction");
let single_authorization = sign_delegation(&deposit_signers[0], sweeper_address, chain_id)?;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's print the minter's nonce every time the minter signs something (number of transactions sent by the minter is critical)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Done in cc57879: the minter's nonce is printed for every transaction it signs (CkSweeper deployment and both sweeps; the CEX deployment nonce is printed too for completeness).

gregorydemay and others added 8 commits July 3, 2026 15:05
- separate wallets for the minter and the CEX (unrelated parties)
- rename *_PK constants to *_PRIVATE_KEY
- print the raw signed transaction hex of each sweep
- print the minter's nonce for every transaction it signs
- hard-code the expected gas used by both sweep transactions
- fix the EthBalance field reference in the design doc

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…esign

The sweeper delegate can call the existing helper contract
(DepositHelperWithSubaccount.sol) instead of transferring directly: the
sweep then emits the canonical ReceivedEthOrErc20 event, so the minter's
existing crediting pipeline is reused unchanged and deposit detection is
demoted to a sweep-scheduling hint. Since the principal becomes a sweep
argument, sweeping is restricted to the minter. Sweeps can be scheduled
on deposits observed at the latest block without waiting for finality:
crediting only follows the finalized helper event, so a reorg merely
wastes gas. The decision between direct sweep (A) and sweep-through-
helper (B) is left open in the design doc.

The demo exercises both variants against the real helper bytecode,
asserts the emitted events carry the right principals, and shows a
non-minter sweep attempt being rejected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Registering a deposit address must trigger no tECDSA signature and no
Ethereum transaction: registrations are free for callers, so any eager
per-address spending would let an attacker drain the minter's cycles
and ETH. Delegation is signed and the sweep submitted only after a
balance of a supported token >= the per-token minimum has been observed
at the registered address, and balance scanning itself stays
claim-driven/bounded since the registered set is attacker-inflatable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Observing balances of many registered deposit addresses (the R13 gate)
must not cost one HTTPS outcall per address and provider. Record the
dependency on the EVM-RPC canister eth_batch endpoint
(dfinity/evm-rpc-canister#561, in progress) and the Multicall3
aggregate3 alternative usable meanwhile.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A native-ETH deposit carries its own gas, so a dedicated ETH deposit
address (second derivation schema tag) never needs EIP-7702 at all: the
minter sweeps it with a plain 21k-gas transfer signed by the address'
own derived key, paying gas from the swept balance. The address never
carries code, so fixed-21000-gas CEX withdrawals always succeed and R12
holds trivially. The set-and-clear delegation lifecycle on a shared
address is demoted to a fallback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sweep transactions are paid in ETH by the minter, but that ETH backs
ckETH 1:1. New requirement R14: burn-first from the minter's fee
account on the ckETH ledger, at least the transaction's maximum fee;
track burned-but-unspent as prepaid credit for subsequent burns, never
re-mint. Deposit fees are minted to a per-token fee account (full
deposited amount minted, supply stays equal to backing); converting
that per-token revenue into ckETH to replenish the fee account is a
treasury operation out of scope.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mermaid sequence diagrams for the full flow: ckUSDT under variant A
(direct sweep, mint on finalized deposit) and variant B (sweep through
the helper, existing pipeline mints), and ckETH in Phase 2 (dedicated
never-delegated address, deposit pays its own sweep gas). Diagramming
surfaced a gap now closed: ETH sweeps need no R14 burn, but under
variant A the sweep's max fee must be capped by the charged deposit fee
since crediting happens before the sweep.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mermaid treats ';' as a statement separator, so semicolons inside note
text split the note into an invalid statement. All three diagrams now
validated with mermaid-cli.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants